﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Collections.ObjectModel;
using System.IO;
using Microsoft.Cci;
using AndersLiu.Reflector.Core;

namespace AndersLiu.Reflector.Program
{
    /// <summary>
    /// Indicates a workspace in the Reflector.
    /// </summary>
    class Workspace
    {
        /// <summary>
        /// Create a new, empty workspace.
        /// </summary>
        public Workspace()
        {
            this.Title = "[Untitled Workspace]";
            this.Version = _currentVersion;
            this.Path = null;
            this.Units = new ReadOnlyCollection<WorkspaceUnit>(_units);
            this.IsChanged = false;
        }

        /// <summary>
        /// Get the title of the workspace.
        /// </summary>
        /// <remarks>
        /// <para>The worksapce title is not saved into the workspace itself,
        /// but generated by the program.</para>
        /// </remarks>
        public string Title { get; private set; }

        /// <summary>
        /// Get the version of the current workspace.
        /// </summary>
        public Version Version { get; private set; }

        /// <summary>
        /// Get the file path of the saved workspace.
        /// Null value indicates a newly created workspace, which has not been saved.
        /// </summary>
        public string Path { get; private set; }

        /// <summary>
        /// Get the assembly files' path.
        /// </summary>
        public ReadOnlyCollection<WorkspaceUnit> Units { get; private set; }

        /// <summary>
        /// Get a value, indicates wether the workspace has been changed since last saveing.
        /// </summary>
        public bool IsChanged { get; private set; }

        /// <summary>
        /// Get a value, indicates wether the workspace is newly created.
        /// </summary>
        public bool IsNew { get { return this.Path == null; } }

        /// <summary>
        /// Save the current workspace to the specified file.
        /// </summary>
        /// <param name="path">Path of the workspace file.</param>
        public void Save(string path)
        {
            if (path == null)
                throw new ArgumentNullException("path");

            using (var writer = new XmlTextWriter(path, _encoding))
            {
                writer.Indentation = 1;
                writer.IndentChar = '\t';
                writer.Formatting = Formatting.Indented;

                writer.WriteStartDocument();
                writer.WriteStartElement("ReflectorWorkspace");

                writer.WriteStartAttribute("Version");
                writer.WriteValue(this.Version.ToString(2));
                writer.WriteEndAttribute();  // Version

                foreach (var unit in this.Units)
                {
                    writer.WriteStartElement("Unit");
                    writer.WriteStartAttribute("Location");
                    writer.WriteValue(unit.Location);
                    writer.WriteEndAttribute();  // Path
                    writer.WriteEndElement();  // File
                }

                writer.WriteEndElement();  // ReflectorWorkspace
                writer.WriteEndDocument();

                AssociateWithFile(path);
                this.IsChanged = false;
                OnChanged(new WorkspaceChangedEventArgs(WorkspaceChangedReason.Saved, null));
            }
        }

        /// <summary>
        /// Save the current workspace to the previous saved location.
        /// </summary>
        /// <exception cref="NotSupportedException">
        /// Thrown when the current workspace haven't been saved before.
        /// </exception>
        public void Save()
        {
            if (this.Path == null)
                throw new NotSupportedException();

            Save(this.Path);
        }

        /// <summary>
        /// Load a workspace object from the specified file.
        /// </summary>
        /// <param name="path">Path of the workspace file.</param>
        /// <returns>The workspace object.</returns>
        public void Load(string path)
        {
            var xdoc = new XmlDocument();
            xdoc.Load(path);

            var versionNode = xdoc.SelectSingleNode("/ReflectorWorkspace/@Version");
            if (versionNode == null)
                throw new FormatException("Missing version node in the workspace file.");

            var version = new Version(versionNode.Value);
            if (version != _currentVersion)
                throw new FormatException(string.Format(
                    "Current version of the workspace ({0}) is not supported." +
                    "Only version ({1}) is supported.",
                    version, _currentVersion));

            var unitNodes = xdoc.SelectNodes("/ReflectorWorkspace/Unit");

            _units.Clear();
            if (unitNodes != null && unitNodes.Count > 0)
            {
                foreach (XmlNode node in unitNodes)
                {
                    var locNode = node.SelectSingleNode("@Location");
                    if (locNode != null && !string.IsNullOrEmpty(locNode.Value))
                    {
                        var unit = new WorkspaceUnit(locNode.Value);
                        _units.Add(unit);
                    }  // The invalid path will be ignored.
                }
            }

            AssociateWithFile(path);
            this.IsChanged = false;
            OnChanged(new WorkspaceChangedEventArgs(WorkspaceChangedReason.Load, null));
        }

        /// <summary>
        /// Add an assembly file to the current workspace.
        /// </summary>
        /// <param name="location">Path of the assembly file.</param>
        /// <returns>
        /// A bool valud, indicates whether the file is really added.
        /// If a file already exists in the workspace, it wouldn't be added,
        /// and this method returns false without exception.
        /// </returns>
        public bool AddUnit(string location)
        {
            if (location == null)
                throw new ArgumentNullException("location");

            // Check if the file already existed.
            foreach (var unit in this.Units)
                if (string.Compare(unit.Location, location, true) == 0)
                    return false;

            // Add it if not.
            var newUnit = new WorkspaceUnit(location) { IsAdded = true };
            _units.Add(newUnit);
            this.IsChanged = true;
            OnChanged(new WorkspaceChangedEventArgs(WorkspaceChangedReason.UnitAdded, newUnit));

            return true;
        }

        /// <summary>
        /// Remove an assembly file from the current workspace.
        /// </summary>
        /// <param name="location">Unit to removed.</param>
        /// <returns>
        /// A bool value, indicates whether the file is readlly removed.
        /// If a file doesn't exist in the workspace, it wouldn't be removed,
        /// and this method returns false without exception.
        /// </returns>
        public bool RemoveUnit(string location)
        {
            if (location == null)
                throw new ArgumentNullException("location");

            foreach (var unit in this.Units)
            {
                if (string.Compare(unit.Location, location, true) == 0)
                {
                    return RemoveUnit(unit);
                }
            }

            return false;
        }

        public bool RemoveUnit(WorkspaceUnit unit)
        {
            var removed = _units.Remove(unit);

            if (removed)
            {
                this.IsChanged = true;
                OnChanged(new WorkspaceChangedEventArgs(WorkspaceChangedReason.UnitRemoved, unit));
            }

            return removed;
        }

        /// <summary>
        /// Fires when the workspace status changed.
        /// </summary>
        public event EventHandler<WorkspaceChangedEventArgs> Changed;

        /// <summary>
        /// Fire the <see cref="StatusChanged"/> event.
        /// </summary>
        /// <param name="e">Event argument.</param>
        protected virtual void OnChanged(WorkspaceChangedEventArgs e)
        {
            if (this.Changed != null)
                this.Changed(this, e);
        }

        /// <summary>
        /// Currently used workspace version.
        /// </summary>
        private static readonly Version _currentVersion = new Version(1, 0);

        /// <summary>
        /// Encoding used to save the workspace file.
        /// </summary>
        private static readonly Encoding _encoding = Encoding.UTF8;

        /// <summary>
        /// Saves the files with in the workspace.
        /// </summary>
        private List<WorkspaceUnit> _units = new List<WorkspaceUnit>();

        /// <summary>
        /// Associate the current workspace object with a file.
        /// </summary>
        /// <param name="path"></param>
        private void AssociateWithFile(string path)
        {
            // Change the title.
            var fileInfo = new FileInfo(path);
            this.Title = fileInfo.Name;

            this.Path = path;
        }
    }
}
